/*
 moxl - Modular XML Library
 Copyright (c) 2007, Malcolm Sparks

 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 2
 of the License, or (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package moxl.xpath.parser;

import jlogging.lookup.Logger;
import moxl.tokenizer.State;
import moxl.tokenizer.Token;
import moxl.tokenizer.TokenStream;
import moxl.tokenizer.TokenType;
import moxl.tokenizer.TokenizerException;
import moxl.xpath.IncompleteLocationPathException;
import moxl.xpath.axes.XPathAxis;
import moxl.xpath.parser.states.XPathParserState;
import moxl.xpath.parser.states.fncall.XPathFunctionCallStartState;
import moxl.xpath.parser.states.locationpath.XPathLocationPathStartState;
import moxl.xpath.parser.states.primaryexpr.XPathPrimaryExpressionStartState;
import moxl.xpath.parser.states.step.XPathStepStartState;
import moxl.xpath.parser.states.step.XPathStepStartWithImpliedAxisState;
import moxl.xpath.parser.types.Expression;
import moxl.xpath.parser.types.FunctionCall;
import moxl.xpath.parser.types.NonTerminal;
import moxl.xpath.parser.types.PathExpression;
import moxl.xpath.parser.types.Step;
import moxl.xpath.tokenizer.XPathTokenTypes;

/**
 * The XPath parser is an expression based parser. Primary expressions are considered to be simple expressions that do not involve operators. For the purposes
 * of paths, / and // tokens are not considered to be operators. Normal expressions are combinations of primary expressions, with consideration for operator
 * precedence and associativity.
 */
public class XPathParser
{
	private static final Logger logger = Logger.getLogger(XPathParser.class);

	XPathParserState state;

	public PathExpression parseLocationPathFromStream(TokenStream tstream) throws XPathParserException
	{
		PathExpression path = new PathExpression();
		state = XPathLocationPathStartState.getInstance();
		runStateMachine(tstream, path);
		return path;
	}

	public Step parseStepFromStream(TokenStream tstream) throws XPathParserException
	{
		Step step = new Step();
		state = XPathStepStartState.getInstance();
		runStateMachine(tstream, step);
		return step;
	}

	public Step parseStepFromStreamWithImpliedAxis(TokenStream tstream, XPathAxis impliedAxis) throws XPathParserException
	{
		Step step = new Step();
		step.setAxis(impliedAxis);
		state = XPathStepStartWithImpliedAxisState.getInstance();
		runStateMachine(tstream, step);
		return step;
	}

	/**
	 * Consume stream to produce a primary expression. A primary expression is an expression that does not involve any operators.
	 * 
	 * @throws XPathParserException
	 */
	public Operand parsePrimaryExpressionFromStream(TokenStream tstream) throws XPathParserException
	{
		Expression expr = new Expression();
		state = XPathPrimaryExpressionStartState.getInstance();
		runStateMachine(tstream, expr);
		return expr.getFirstOperand();
	}

	private Token lookahead(TokenStream tstream, int amount) throws XPathParserException
	{
		try
		{
			return tstream.lookahead(amount);
		}
		catch (TokenizerException e)
		{
			throw new XPathParserException("Failed to lookahead(" + amount + ")", e);
		}
	}

	private Token read(TokenStream tstream) throws XPathParserException
	{
		try
		{
			return tstream.read();
		}
		catch (TokenizerException e)
		{
			throw new XPathParserException("Failed to read from token stream", e);
		}
	}

	public Expression parseExpressionFromStream(TokenStream tstream) throws XPathParserException
	{
		Expression expr = new Expression();

		// Check for an initial unary
		Token lookahead = lookahead(tstream, 1);
		if (lookahead.getType().equals(XPathTokenTypes.OPERATOR) && lookahead.getImage().equals("-"))
		{
			read(tstream);
			expr.addOperator(AbstractOperator.MINUS);
		}

		expr.addOperand(parsePrimaryExpressionFromStream(tstream));

		lookahead = lookahead(tstream, 1);
		while (lookahead != null && lookahead.getType().equals(XPathTokenTypes.OPERATOR))
		{
			Token tok = read(tstream);
			AbstractOperator operator = AbstractOperator.getOperator(tok.getImage());
			expr.addOperator(operator);

			lookahead = lookahead(tstream, 1);
			if (lookahead.getType().equals(XPathTokenTypes.OPERATOR) && lookahead.getImage().equals("-"))
			{
				read(tstream);
				expr.addOperator(AbstractOperator.MINUS);
			}

			expr.addOperand(parsePrimaryExpressionFromStream(tstream));
			lookahead = lookahead(tstream, 1);
		}

		return expr;
	}

	public FunctionCall parseFunctionCallFromStream(TokenStream tstream) throws XPathParserException
	{
		FunctionCall fnCall = new FunctionCall();
		state = XPathFunctionCallStartState.getInstance();
		runStateMachine(tstream, fnCall);
		return fnCall;
	}

	private void runStateMachine(TokenStream tstream, NonTerminal nonTerminal) throws XPathParserException
	{
		while (!state.isFinishState())
		{
			Token token;
			token = read(tstream);
			if (token == null)
			{
				logger.warn("Null token read from stream in state " + state);
				throw new IncompleteLocationPathException();
			}
			TokenType type = token.getType();

			if (type.equals(XPathTokenTypes.AXIS_NAME))
			{
				state.acceptAxisName(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.DOUBLE_COLON))
			{
				state.acceptDoubleColon(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.NAME_TEST))
			{
				state.acceptNameTest(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.OPERATOR))
			{
				state.acceptOperator(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.OPEN_BRACKET))
			{
				state.acceptOpenBracket(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.CLOSE_BRACKET))
			{
				state.acceptCloseBracket(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.OPEN_PARENTHESIS))
			{
				state.acceptOpenParenthesis(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.CLOSE_PARENTHESIS))
			{
				state.acceptCloseParenthesis(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.AT_SYMBOL))
			{
				state.acceptAtSymbol(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.COMMA))
			{
				state.acceptComma(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.LITERAL))
			{
				state.acceptLiteral(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.NUMBER))
			{
				state.acceptNumber(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.FUNCTION_NAME))
			{
				state.acceptFunctionName(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.VARIABLE_REFERENCE))
			{
				state.acceptVariableReference(this, tstream, token, nonTerminal);
			}
			else if (type.equals(XPathTokenTypes.NODE_TYPE))
			{
				state.acceptNodeType(this, tstream, token, nonTerminal);
			}
			else
			{
				throw new XPathParserException("Parser cannot handle token: " + token);
			}
		}
	}

	public void changeState(State state)
	{
		this.state = (XPathParserState) state;
	}

	public XPathParser begetParser()
	{
		return new XPathParser();
	}

}
